/*
 * Copyright 2007 ZXing authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.zxing.qrcode.decoder;

import java.util.Map;

import com.google.zxing.ChecksumException;
import com.google.zxing.DecodeHintType;
import com.google.zxing.FormatException;
import com.google.zxing.ReaderException;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.common.DecoderResult;
import com.google.zxing.common.reedsolomon.GenericGF;
import com.google.zxing.common.reedsolomon.ReedSolomonDecoder;
import com.google.zxing.common.reedsolomon.ReedSolomonException;

/**
 * <p>
 * The main class which implements QR Code decoding -- as opposed to locating and extracting the QR Code from an image.
 * </p>
 *
 * @author Sean Owen
 */
public final class Decoder {

	private final ReedSolomonDecoder rsDecoder;

	public Decoder() {
		rsDecoder = new ReedSolomonDecoder(GenericGF.QR_CODE_FIELD_256);
	}

	public DecoderResult decode(boolean[][] image) throws ChecksumException, FormatException, ReaderException {
		return decode(image, null);
	}

	/**
	 * <p>
	 * Convenience method that can decode a QR Code represented as a 2D array of booleans. "true" is taken to mean a black module.
	 * </p>
	 *
	 * @param image booleans representing white/black QR Code modules
	 * @param hints decoding hints that should be used to influence decoding
	 * @return text and bytes encoded within the QR Code
	 * @throws FormatException if the QR Code cannot be decoded
	 * @throws ChecksumException if error correction fails
	 */
	public DecoderResult decode(boolean[][] image, Map<DecodeHintType, ?> hints) throws ChecksumException, FormatException, ReaderException {
		int dimension = image.length;
		BitMatrix bits = new BitMatrix(dimension);
		for (int i = 0; i < dimension; i++) {
			for (int j = 0; j < dimension; j++) {
				if (image[i][j]) {
					bits.set(j, i);
				}
			}
		}
		return decode(bits, hints);
	}

	public DecoderResult decode(BitMatrix bits) throws ChecksumException, FormatException, ReaderException {
		return decode(bits, null);
	}

	/**
	 * <p>
	 * Decodes a QR Code represented as a {@link BitMatrix}. A 1 or "true" is taken to mean a black module.
	 * </p>
	 *
	 * @param bits booleans representing white/black QR Code modules
	 * @param hints decoding hints that should be used to influence decoding
	 * @return text and bytes encoded within the QR Code
	 * @throws FormatException if the QR Code cannot be decoded
	 * @throws ChecksumException if error correction fails
	 */
	public DecoderResult decode(BitMatrix bits, Map<DecodeHintType, ?> hints) throws FormatException, ChecksumException, ReaderException {

		// Construct a parser and read version, error-correction level
		BitMatrixParser parser = new BitMatrixParser(bits);
		FormatException fe = null;
		ChecksumException ce = null;
		try {
			return decode(parser, hints);
		} catch (FormatException e) {
			fe = e;
		} catch (ChecksumException e) {
			ce = e;
		}

		try {

			// Revert the bit matrix
			parser.remask();

			// Will be attempting a mirrored reading of the version and format info.
			parser.setMirror(true);

			// Preemptively read the version.
			parser.readVersion();

			// Preemptively read the format information.
			parser.readFormatInformation();

			/*
			 * Since we're here, this means we have successfully detected some kind of version and format information when mirrored. This is a good sign, that the QR code may be mirrored, and we
			 * should try once more with a mirrored content.
			 */
			// Prepare for a mirrored reading.
			parser.mirror();

			DecoderResult result = decode(parser, hints);

			// Success! Notify the caller that the code was mirrored.
			result.setOther(new QRCodeDecoderMetaData(true));

			return result;

		} catch (ReaderException e) {
			// Throw the exception from the original reading
			if (e instanceof FormatException || e instanceof ChecksumException) {
				if (fe != null) {
					throw fe;
				}
				if (ce != null) {
					throw ce;
				}
			}
			throw e;
		}
	}

	private DecoderResult decode(BitMatrixParser parser, Map<DecodeHintType, ?> hints) throws FormatException, ChecksumException {
		Version version = parser.readVersion();
		ErrorCorrectionLevel ecLevel = parser.readFormatInformation().getErrorCorrectionLevel();

		// Read codewords
		byte[] codewords = parser.readCodewords();
		// Separate into data blocks
		DataBlock[] dataBlocks = DataBlock.getDataBlocks(codewords, version, ecLevel);

		// Count total number of data bytes
		int totalBytes = 0;
		for (DataBlock dataBlock : dataBlocks) {
			totalBytes += dataBlock.getNumDataCodewords();
		}
		byte[] resultBytes = new byte[totalBytes];
		int resultOffset = 0;

		// Error-correct and copy data blocks together into a stream of bytes
		for (DataBlock dataBlock : dataBlocks) {
			byte[] codewordBytes = dataBlock.getCodewords();
			int numDataCodewords = dataBlock.getNumDataCodewords();
			correctErrors(codewordBytes, numDataCodewords);
			for (int i = 0; i < numDataCodewords; i++) {
				resultBytes[resultOffset++] = codewordBytes[i];
			}
		}

		// Decode the contents of that stream of bytes
		return DecodedBitStreamParser.decode(resultBytes, version, ecLevel, hints);
	}

	/**
	 * <p>
	 * Given data and error-correction codewords received, possibly corrupted by errors, attempts to correct the errors in-place using Reed-Solomon error correction.
	 * </p>
	 *
	 * @param codewordBytes data and error correction codewords
	 * @param numDataCodewords number of codewords that are data bytes
	 * @throws ChecksumException if error correction fails
	 */
	private void correctErrors(byte[] codewordBytes, int numDataCodewords) throws ChecksumException {
		int numCodewords = codewordBytes.length;
		// First read into an array of ints
		int[] codewordsInts = new int[numCodewords];
		for (int i = 0; i < numCodewords; i++) {
			codewordsInts[i] = codewordBytes[i] & 0xFF;
		}
		int numECCodewords = codewordBytes.length - numDataCodewords;
		try {
			rsDecoder.decode(codewordsInts, numECCodewords);
		} catch (ReedSolomonException ignored) {
			throw ChecksumException.getChecksumInstance();
		}
		// Copy back into array of bytes -- only need to worry about the bytes that were data
		// We don't care about errors in the error-correction codewords
		for (int i = 0; i < numDataCodewords; i++) {
			codewordBytes[i] = (byte) codewordsInts[i];
		}
	}

}
